#!/bin/bash

usage() {
   cat <<EOF

Get weather data from FMI's mobile data service.
Last non-option argument: 

Most options can also be shell-sourced from $conf

Usage: ./$me [options] [location_id]

Dependencies: ${deps[@]}

Options:

-L int  numerical location ID (e.g. from geonames.org; you can use the provided
        get_location_id script). If no location ID is given Helsinki is used.
        Default: geoid=$geoid

-l str  language, possible values: en,fi,sv
        Default: lang=$lang

-i str  comma-separated list of indices (positive whole numbers) to get
        forecasts for. The default is to get them for every hour for the first
        two hours, then every other hour twice, then every three hours. No index
        must be larger than $max_index.
        Default: indices=$indices

-f str  Formatted output templates specifier.
        Will use header."string", forecast."string" and footer."string"
        templates from storage folder (see below, -s option).
        Currently "lua" is the only already present set of templates
        Default: format='' (terminal output)

-o str  Comma-separated list of options to pass to formatting section, to
        override one or several defaults.
        Example: "offset=50,img_size_fc=32,padding_h=10,font=droid\ sans\ mono"

        width  - - - - - (px) overall width of output, padding excluded (you
                         will still need to adjust your conky.config manually -
                         default: $width)
        padding_h  - - - (px) horizontal padding (default: $padding_h)
        offset - - - - - (px) initial offset for forecasts - or: height of the
                         header (default: $offset). This value is incremented
                         with each forecast element.
        img_size_fc  - - (px) width and height of forecast image (default: $img_size_fc)
        img_size_wind  - (px) width and height of wind image (default: $img_size_wind)
        forecast_height  (px) height of each forecast element
                         (default: same as img_size_fc)
        tempcol_plus - - (hex) color for temperature >= 0 (default: $tempcol_plus)
        tempcol_minus  - (hex) color for temperature < 0 (default: $tempcol_minus)
        textcol  - - - - (hex) color for other text (default: $textcol)
        linecol  - - - - (hex) color for line separator and time
                         (default: $linecol)
        popcol70 - - - - (hex) color for PoP (chance of precipitation)
                         when percentage is >=70 (default: $popcol70)
        popbg30  - - - - (hex) background color for PoP when percentage is >=30
                         (default: $popbg30)
        popbg70  - - - - (hex) background color for PoP when percentage is >=70
                         (default: $popbg70)
        font - - - - - - (str) font to use for all text (default: $font)
        Default: options=''

-g      Include wind gusts in forecast
        (comes from a different source: $urlbase_gusts)
        Default: gusts=$gusts

-p      Convert images to PNG instead of using native SVG
        Default: usepng=$usepng

-r      Remove cached weather data first

-R      Remove all cached data first

EOF
   exit 0
}
ex_err() {
   [ -n "$opt" ] && printf '%s' "-${opt}: " 
   [ -n "$*" ] && printf '%s\n' "$*"
   printf 'Try %s -h\n%b' "$0"
   exit 1
}

dl_smartsymbol() {
   # Check if smartsymbol SVG exists, otherwise download it, then check again
   # $1 full path to desired output
   [ -r "$1" ] && return 0
   curl --output "$1" --user-agent "$useragent" "$urlbase_img_fc/${1##*/}" >&2
   [ -r "$1" ] || usage "Oof. Could not download required image $1 from $urlbase_img_fc/${1##*/}"
}
dl_interval() {
   # $1: file to download to
   # $2: URL
   # will download a file if its age starts with the previous full hour
   # meaning: the threshold to re-download is not a full hour, but the start of a new hour
   if ! [ -r "$1" ]; then
      echo "Downloading for the first time: $2" >&2
      curl --user-agent "$useragent" -o "$1" "$2" >&2 || usage "Something went wrong. Bailing."
   elif check_hour "$1"; then
      echo "Updating $1 - downloading: $2" >&2
      curl --user-agent "$useragent" -o "$1" "$2" >&2 || usage "Something went wrong. Bailing."
   else
      echo "No new hour has started & file $1 exists." >&2
   fi
}
get_ss_text() {
   # get forecast text according to smartsymbol, from symbol_$lang file
   # $1: smartsymbol number
   while read line; do
      [[ "$line" == "$1"* ]] && RETVAL="${line#*:}" && break
   done <"$symbol_text$lang"
}
make_wind_svg() {
   # replaces strings in SVG data string with calculated values
   wind_svg="${wind_svg_base/@wind_deg@/$((wind_deg-180))}"
   wind_svg="${wind_svg//@circle_color@/$circle_color}"
   wind_svg="${wind_svg//@arrow_color@/$arrow_color}"
}
check_hour() {
   # $1: file to check
   # just return whether the current hour is at least 1 more than the file's age
   # because forecasts are updated on the full hour
   (( "$HOUR" > "$(date '+%y%m%d%H' --date "$(stat -c '%y' "$1")")" ))
}

reset="\e[0;0m" # reset terminal to normal colours
me="${0##*/}"
realme="$(realpath "$0")"
HOUR="$(date '+%y%m%d%H')" # the current hour, to decide whether re-downloading is required
urlbase="https://widget.weatherproof.fi/android/androidwidget.php"
urlbase_gusts="https://opendata.fmi.fi"
urlbase_img_fc="https://cdn.fmi.fi/symbol-images/smartsymbol/v31/p"
RETVAL='' # global variable to store functions' return values
deps=( "curl:" "required," "jshon:" "required," "stat:" "required," "date:" "required," "sed:" "for conky formatted output," "rsvg-convert:" "to convert SVGs to PNGs" )
max_index=220 # That many forecasts are contained in the json

# files, storage, static and changing data
cache="${XDG_CACHE_HOME-"$HOME/.cache"}/$me"
conf="${XDG_CONFIG_HOME-"$HOME/.config"}/$me/conf"
templates="${realme%/*}/templates"
lib="${realme%/*}/lib"
symbol_text="$lib/symbol_text_" # 2-letter language code will be appended

geoid=658225 # Helsinki
lang=fi
gusts=1
indices="0,1,3,6,10,15,21,28,36,45,55"
format=''
usepng=0
### options (see help text for -o option) ###
options=''
### what options haven't been set by users will be set here
width=330
padding_h=10
offset=30
img_size_fc=64
img_size_wind=30
tempcol_plus="ee0000"
tempcol_minus="1e90ff"
textcol="303193"
linecol="888888"
font="sans-serif"
popcol70="ffffff"
popbg30="A1C8E6"
popbg70="3A66E3"
bg="#FAFCFE" # window background

# wind_svg_base must contain the invalid string "@wind_deg@" which will be replaced with the actual rotation, also
# @arrow_color@ and @circle_color@
#~ wind_svg_base='<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><g fill="none" fill-rule="evenodd" transform="rotate(@wind_deg@ 15 15)"><circle cx="15" cy="15" r="10" fill="@circle_color@" stroke="@arrow_color@" stroke-width="2"/><path fill="@arrow_color@" fill-rule="nonzero" d="M14.999 5L20 12 16 12 16 25 14 25 14 11.999 10 11.999z"/></g></svg>'
# without stroke
wind_svg_base='<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><g fill="none" fill-rule="evenodd" transform="rotate(@wind_deg@ 15 15)"><circle cx="15" cy="15" r="13" fill="@circle_color@" /><path fill="@arrow_color@" fill-rule="nonzero" d="M14.999 5L20 12 16 12 16 25 14 25 14 11.999 10 11.999z"/></g></svg>'

# Mobile User Agent
# curl "https://www.whatismybrowser.com/guides/the-latest-user-agent/android" | xmllint --html --nonet --xpath "//table//li//text()" - 2>/dev/null | head -1
useragent="Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.4.6478.71 Mobile Safari/537.36"

printf '%b' "\e[33m" >&2  # change color to yellow until optionparsing & data aquisition is done

# dependency check
for ((i=0;i<${#deps[@]};i+=2)); do
	type -f ${deps[i]%:} >/dev/null 2>&1 || { [[ "${deps[i+1]%,}" == "required" ]] && usage "Missing dependency: ${deps[i]%:}" || echo "Missing optional dependency: ${deps[i]%:} (${deps[i+1]%,})" >&2; }
done

[ -r "$conf" ] && source "$conf"

while getopts "L:l:i:f:o:gprRh" opt; do
  case $opt in
    L) [[ "$OPTARG" =~ [0-9]+ ]] || ex_err "Invalid location ID: $OPTARG"
       geoid="$OPTARG"
    ;;
    l) [[ "$OPTARG" =~ ^(fi|en|sv)$ ]] || ex_err "Invalid language: $OPTARG"
       lang="$OPTARG"
    ;;
    i) { [[ "${OPTARG//,/}" =~ ^[0-9]+$ ]] && [[ "$OPTARG" =~ ^[0-9].*[0-9]$ ]]; } || ex_err "-${opt}: invalid option $OPTARG"
       indices="$OPTARG"
    ;;
    f) [[ "$OPTARG" =~ ^[0-9a-zA-Z]+$ ]] || ex_err "invalid option $OPTARG"
       format="$OPTARG"
    ;;
    o) options="$OPTARG"
    ;;
    g) gusts=1
    ;;
    p) usepng=1
    ;;
    r) rm -f "$cache/"*.{json,xml}
    ;;
    R) rm -rf "$cache/"
    ;;
    h) usage
    ;;
    *) unset opt; ex_err
    ;;
  esac
done
max="${indices##*,}"

mkdir -p "$cache"
file_forecast="$cache/forecast-$geoid.json"
file_gusts="$cache/gusts-$geoid.xml"

if [ -n "$options" ]; then
   # convert -o "options" string to actual variables
   oldifs="$IFS"; IFS=, options=( $options ); IFS="$oldifs"
   for ((i=0;i<${#options[@]};i++));do
      var="${options[i]%%=*}"
      val="${options[i]##*=}"
      declare "$var"="$val" || {
         opt=o ex_err" Could not set variable \"$var\" to value \"$val\"."
         }
   done
fi
# variables that need to be set after parsing options
forecast_height="${forecast_height-"$img_size_fc"}"

# declare this array even if it isn't used in the end
declare -A wg # windgusts indexed by local timestamps
###########################
####### DATA AQUISITION ###
### Load gusts XML and extract values ###
if [[ "$gusts" == 1 ]]; then
   # type of forecast, get a list with ./wfs_describeStoredQueries
   #Harmonie Point Weather Forecast as multipointcoverage (fmi::forecast::harmonie::surface::point::multipointcoverage)
   #Harmonie Point Weather Forecast as simple features (fmi::forecast::harmonie::surface::point::simple)
   #Harmonie Point Weather Forecast as time value pairs (fmi::forecast::harmonie::surface::point::timevaluepair)
   query="fmi::forecast::harmonie::surface::point::simple"
   wfs_req="wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id"
   starttime="$(date -u '+%FT%R:%SZ')" # now, in UTC
   endtime="$(date -u '+%FT%R:%SZ' --date="$((max + 1)) hours")" # maximum results. Have to indcrease by 1 becasue forecasts are always on the full hour.
   timestep=60 # always 60 minutes, because that's what the mobile service offers
   URL="$urlbase_gusts/$wfs_req=${query}&geoid=${geoid}&starttime=${starttime}&endtime=${endtime}&timestep=${timestep}&parameters=windgust"
   #~ curl -s -o "$file_xml" "$URL"
   #~ curl -s -o "$file_forecast" --user-agent "$useragent" "${urlbase}?l=${lang}&locations=${geoid}"
   #~ exit
   dl_interval "$file_gusts" "$URL"
   xml="$(<"$file_gusts")"
   while read line; do
   # avoid dependency for XML parsing, quick'n'dirty line by line instead:
   [[ "$line" == *'<BsWfs:Time>'* ]] && {
      index="${line#*>}"
      index="${index%%<*}"
      index="$(date '+%Y%m%dT%H%M%S' --date "$index")"
      continue
      }
   [[ "$line" == *'<BsWfs:ParameterValue>'* ]] && {
      line="${line#*>}"
      line="${line%%<*}"
      { [[ "${line#*.}" == "0"* ]] || (( ${line#*.} < 50 )); } && line=${line%.*} || line=$(( ${line%.*} + 1 ))
      wg[$index]="$line"
      }
   done <<<"$xml"
fi

### Load weather forecast JSON ###
URL="${urlbase}?l=${lang}&locations=${geoid}"
dl_interval "$file_forecast" "$URL"
json="$(<"$file_forecast")"

### Associative array to house all forecasts' key/value pairs
declare -A output

# This we need only once, the name of the place, and its region:
name="$(jshon -e forecasts -e 0 -e forecast -e 0 -e name -u -p -e region -u <<<"$json")"
region="${name##*$'\n'}"
name="${name%%$'\n'*}"

# This we need for every hourly forecast index:
keys=(localtime Temperature FeelsLike SmartSymbol PoP WindSpeedMS WindDirection WindCompass8 Precipitation1h)
units=(""       "°C"        "°C"        ""        "%"   "m/s"     "°"            ""          "mm") # applied later
for ((i=0;i<${#keys[@]};i++)); do
   j=0
   while read line; do 
      output[${keys[i]} $((j++))]="$line"
   done <<<"$(jshon -Q -e forecasts -e 0 -e forecast -a -e "${keys[i]}" -u <<<"$json")"
done

printf '%b' "$reset" >&2 # reset colors

################################################################################
####### DATA OUTPUT & FORMATTING #############
indices=( ${indices//,/ } ) # transform comma-separated list to array
declare -A weekdays=( 
   [en1]=Mon        [en2]=Tue         [en3]=Wed           [en4]=Thu          [en5]=Fri        [en6]=Sat          [en7]=Sun
   [en1long]=Monday [en2long]=Tuesday [en3long]=Wednesday [en4long]=Thursday [en5long]=Friday [en6long]=Saturday [en7long]=Sunday
   [fi1]=ma         [fi2]=ti          [fi3]=ke            [fi4]=to           [fi5]=pe         [fi6]=la           [fi7]=su
   [fi1long]=maanantai [fi2long]=tiistai [fi3long]=keskiviikko [fi4long]=torstai [fi5long]=perjantai [fi6long]=lauantai [fi7long]=sunnuntai
   [sv1]=mån        [sv2]=tis         [sv3]=ons           [sv4]=tor          [sv5]=fre        [sv6]=lör          [sv7]=sön
   [sv1long]=måndag [sv2long]=tisdag  [sv3long]=onsdag    [sv4long]=torsdag  [sv5long]=fredag [sv6long]=lördag   [sv7long]=söndag
)
long="" # set to "long" to use long names
upper=0 # set to 0 to use days as they are, without capitalizing them
if [ -z "$format" ]; then
### Simple CLI output ###
   sep="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯"
   ### HEADING ###
   printf '%s, %s\n%s\n' "$name" "$region" "$sep"
   ### FORECAST ###
   for i in ${indices[@]}; do
      for ((j=0;j<${#keys[@]};j++)); do
         [[ "${keys[j],,}" == smartsymbol ]] && {
            # get weather icon description from symbol_ file
            get_ss_text "${output[${keys[j]} $i]}"
            echo "${keys[j]}: $RETVAL"
            continue
         }
         echo "${keys[j]}: ${output[${keys[j]} $i]}${units[j]}"
      done
      [[ "$gusts" == 1 ]] && echo "WindGusts: ${wg[${output[localtime $i]}]}"
      echo "$sep"
   done
else
### Output with images etc. (conky) ###
   ### check for or create different subfolders for SVG and PNG images
   smartsymbols_png="$cache/smartsymbol_png_$img_size_fc"
   windsymbols_png="$cache/windsymbol_png_$img_size_wind"
   smartsymbols_svg="$cache/smartsymbol_svg"
   windsymbols_svg="$cache/windsymbol_svg"
   mkdir -p "$smartsymbols_svg" "$windsymbols_svg" || ex_err "Could not find or create subfolders in \"$cache\""
   [[ "$usepng" == 1 ]] && { mkdir -p "$smartsymbols_png" "$windsymbols_png" || ex_err "Could not find or create subfolders in \"$cache\""; }

   template_header="$templates/header.$format"
   template_forecast="$templates/forecast.$format"
   template_footer="$templates/footer.$format"

   # /^[ \t]*#/d; /^[ \t]*--/d = remove lua comments
   sed_script_base="s|%textcol%|$textcol|g
   s|%bg%|$bg|g
   s|%linecol%|$linecol|g
   s|%padding_h%|$padding_h|g
   s|%font%|$font|g
   s|%img_size_fc%|$img_size_fc|g
   s|%img_size_wind%|$img_size_wind|g
   s|%width%|$width|g
   s|%forecast_height%|$forecast_height|g"

   ### HEADER ###
   [ -r "$template_header" ] && sed "$sed_script_base
      s|%offset%|$offset|g
      s|%name%|$name|
      s|%region%|$region|" "$template_header"

   ### FORECASTS ###
   [ -r "$template_forecast" ] && { # all of this only happens if there's a template file to iterate this over
      oldday="${output[localtime ${indices[0]}]:4:4}"
      for i in ${indices[@]}; do
         time="${output[localtime $i]:9:2}:${output[localtime $i]:11:2}"
         day="${output[localtime $i]:4:4}"
         # if a new day begins, show it as a weekday, not a numerical date.
         if [[ "$day" != "$oldday" ]]; then
            oldday="$day"
            day="$(date +%u --date="${output[localtime $i]:0:8}")"
            day="${weekdays[$lang$day$long]}"
            [[ "$upper" == 1 ]] && day="${day^}"
         else
            day=""
         fi
         # smartsymbol (an integer)
         ss="${output[SmartSymbol $i]}"
         # get forecast text corresponding to smartsymbol
         get_ss_text "$ss"
         ss_text="$RETVAL"
         
         # adjust temperature color depending on temperature plus or minus
         (( ${output[Temperature $i]} < 0 )) && tempcol="$tempcol_minus" || tempcol="$tempcol_plus"
   
         ### now prepare images & colors ###

         # rotate wind svg (data)
         wind_deg="${output[WindDirection $i]}"
         wind_speed="${output[WindSpeedMS $i]}"
         # rounding up/down to end on 0, so that we have only 36 directions instead of 360
         (( ${wind_deg: -1} >= 5 )) && wind_deg="$(( ${wind_deg%?} + 1 ))0" || wind_deg="${wind_deg%?}0"
         # colorize SVG acccording to wind strength
         if (( wind_speed >= 21 )); then
            circle_color="#000000"; arrow_color="#ffffff"; level="level-04"
         elif (( wind_speed >= 14 )); then
            circle_color="#303193"; arrow_color="#ffffff"; level="level-03"
         elif (( wind_speed >= 8 )); then
            circle_color="#3A66E3"; arrow_color="#ffffff"; level="level-02"
         elif (( wind_speed >= 1 )); then
            circle_color="#E7F0FA"; arrow_color="#303193"; level="level-01"
         else
            circle_color="#E7F0FA"; arrow_color="#E7F0FA"; level="level-00"
         fi

         # colors for PoP background box & text
         #~ pop="${output[PoP $i]}"
         #~ if (( pop >= 70 )); then
            #~ popbg="$popbg70"
            #~ popcol="$popcol70"
         #~ elif (( pop >= 30 )); then
            #~ popbg="$popbg30"
            #~ popcol="$textcol"
         #~ else
            #~ popbg="x"
            #~ popcol="$textcol"
         #~ fi
         # colors for PoP text (no background box)
         pop="${output[PoP $i]}"
         if (( pop >= 70 )); then
            popbg="x"
            popcol="$textcol"
         elif (( pop >= 30 )); then
            popbg="x"
            popcol="$popbg70"
         else
            popbg="x"
            popcol="$popbg30"
         fi

         si_svg="$smartsymbols_svg/$ss.svg"
         # use SVGs
         dl_smartsymbol "$si_svg"
         smart_img="$si_svg"
         wind_img="$windsymbols_svg/deg${wind_deg}-$level.svg"
         [ -r "$wind_img" ] || {
            make_wind_svg
            echo "$wind_svg" > "$wind_img"
         }
         if [[ "$usepng" == 1 ]]; then
            # convert things to PNG
            wind_img="$windsymbols_png/deg${wind_deg}-$level.png"
            [ -r "$wind_img" ] || {
               make_wind_svg
               rsvg-convert -o "$wind_img" --page-width="$img_size_wind" --page-height="$img_size_wind" <<<"$wind_svg"
                #~ --top=$(((img_size-30)/2))  --left=$(((img_size-30)/2))
            }
            smart_img="$smartsymbols_png/$ss.png"
            [ -r "$smart_img" ] || {
               dl_smartsymbol "$si_svg"
               rsvg-convert -o "$smart_img" --width="$img_size_fc" --height="$img_size_fc" "$si_svg"
            }
         fi
         [[ "$gusts" == 1 ]] && {
            gust="${wg[${output[localtime $i]}]}"
            # If gust is the same as windspeed it is not required
            [[ "$gust" == "$wind_speed" ]] || wind_speed="$wind_speed-$gust"
         }
         ### now translate $format templates
         sed "$sed_script_base
         s|%localtime%|$time|
         s|%day%|$day|
         s|%Temperature%|${output[Temperature $i]}${units[1]}|
         s|%FeelsLike%|${output[FeelsLike $i]}${units[2]}|
         s|%PoP%|$pop${units[4]}|
         s|%Precipitation1h%|${output[Precipitation1h $i]}|
         s|%WindSpeedMS%|$wind_speed|
         s|%WindSpeedUnit%|${units[5]}|
         s|%WindCompass8%|${output[WindCompass8 $i]}|
         s|%wind_img%|$wind_img|; s|%offset%|$offset|g
         s|%smart_img%|$smart_img|
         s|%tempcol%|$tempcol|g
         s|%popbg%|$popbg|g
         s|%popcol%|$popcol|g
         s|%ss_text%|$ss_text|; s|%offset%|$offset|g" "$template_forecast"
   
         ### increase offset
         offset=$(( offset + forecast_height ))
      done
   } # END [ -r "$template_forecast" ] &&

   ### FOOTER ###
   [ -r "$template_footer" ] && sed "$sed_script_base; s|%offset%|$offset|g; s|%offset_file%|$cache/offset|g" "$template_footer"
fi
